﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace GojiSoft.EventSequenceUnitTesting {
    public delegate void ProxyEventHandler(object sender, EventArgs e, string eventName);

    public static class DynamicEvent {
        #region Private Methods

        private static DynamicMethod CreateDynamicMethod(Type eventType, string eventName, Type proxyOwner) {
            string proxyName = ("Generated proxy for event: " + eventName + ".");
            Type returnType = typeof(void);

            List<Type> eventParameterTypes = new List<Type>();

            ParameterInfo[] eventParameters = eventType.GetMethod("Invoke").GetParameters();

            eventParameterTypes.Add(proxyOwner);

            eventParameterTypes.AddRange(eventParameters.Select(x => x.ParameterType));

            Type[] parameters = eventParameterTypes.ToArray();

            DynamicMethod proxyMethod = new DynamicMethod(proxyName, returnType, parameters, proxyOwner, true);

            return proxyMethod;
        }

        private static void EmitCode(DynamicMethod proxyMethod, bool isStatic, string eventName, ProxyEventHandler eventHandler) {
            ILGenerator il = proxyMethod.GetILGenerator();

            if(!isStatic) {
                il.Emit(OpCodes.Ldarg_0);                //the target object for non-static calls.
            }

            il.Emit(OpCodes.Ldarg_1);                    //arg0 - sender
            il.Emit(OpCodes.Ldarg_2);                    //arg1 - event args
            il.Emit(OpCodes.Ldstr, eventName);           //arg2 - *injected event name*
            il.Emit(OpCodes.Call, eventHandler.Method);  //call proxy event handler
            il.Emit(OpCodes.Ret);
        }

        #region Validate Event Type

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SomeHandler")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SomeEventArgs")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "EventArgs")]
        private static void ValidateEventType(string eventName, Type eventType) {
            MethodInfo method = eventType.GetMethod("Invoke");

            ParameterInfo[] eventParameters = eventType.GetMethod("Invoke").GetParameters();

            bool isValid = method.ReturnType == typeof(void);

            if(isValid)
                isValid = eventParameters.Length == 2;

            if(isValid)
                isValid = !IsModifiedParameter(eventParameters[0]);
            if(isValid)
                isValid = !IsModifiedParameter(eventParameters[1]);

            if(isValid)
                isValid = eventParameters[0].ParameterType.IsClass;
            if(isValid)
                isValid = eventParameters[1].ParameterType == typeof(EventArgs) || eventParameters[1].ParameterType.IsSubclassOf(typeof(EventArgs));

            if(!isValid) {
                string message = "Cannot bind to non-standard target event '" + eventName + "'.";
                message += Environment.NewLine;
                message += "The event delegate does not conform to standard event design guidelines.";
                message += Environment.NewLine;
                message += "Event delegates should not have a return value and should have 2 arguments which are (or are derived from): object, EventArgs.";
                message += Environment.NewLine;
                message += "Non void return types, ref arguments, and out arguments are not supported.";
                message += Environment.NewLine;
                message += "Expected event delegate signature style: Void SomeHandler(Object sender, SomeEventArgs e).";
                message += Environment.NewLine;
                message += "The target event delegate has unsupported signature: " + method.ReturnType.Name + " " + eventName + GetParamterSignatures(eventParameters);

                throw new InvalidOperationException(message);
            }
        }

        private static string GetParamterSignatures(ParameterInfo[] parameters) {
            StringBuilder sb = new StringBuilder("(");

            for(int i = 0; i < parameters.Length; i++) {
                ParameterInfo parameter = parameters[i];

                if(parameter.IsOut) {
                    sb.Append("out ");
                } else if(IsModifiedParameter(parameter)) {
                    sb.Append("ref ");
                }

                sb.Append(parameter.ParameterType.Name.TrimEnd(new[] { '&' }));
                sb.Append(" ");
                sb.Append(parameter.Name);

                if(i < parameters.Length - 1) {
                    sb.Append(", ");
                }
            }

            sb.Append(")");

            return sb.ToString();
        }

        private static bool IsModifiedParameter(ParameterInfo parameter) {
            bool isModifiedArgument = parameter.ParameterType.Name.EndsWith("&", StringComparison.Ordinal);

            return isModifiedArgument;
        }

        #endregion

        #endregion

        #region Public Methods

        public static Delegate CreateProxyEventHandler(EventInfo eventInfo, object eventPublisher, ProxyEventHandler eventHandler) {
            if(eventInfo == null)
                throw new ArgumentNullException("eventInfo");
            if(eventInfo.EventHandlerType == null)
                throw new ArgumentNullException("eventInfo", "Property EventHandlerType was null.");
            if(eventPublisher == null)
                throw new ArgumentNullException("eventPublisher");
            if(eventHandler == null)
                throw new ArgumentNullException("eventHandler");

            Type eventType = eventInfo.EventHandlerType;
            bool isStatic = eventHandler.Method.IsStatic;
            Type proxyOwner = isStatic ? eventHandler.GetType() : eventHandler.Target.GetType();
            string eventName = eventInfo.Name;

            ValidateEventType(eventName, eventType);

            DynamicMethod proxyMethod = CreateDynamicMethod(eventType, eventName, proxyOwner);

            EmitCode(proxyMethod, isStatic, eventName, eventHandler);

            Delegate proxyEventHandler = proxyMethod.CreateDelegate(eventType, eventHandler.Target);

            return proxyEventHandler;
        }

        public static void Subscribe(EventInfo eventInfo, object eventPublisher, ProxyEventHandler eventHandler) {
            Delegate proxyEventHandler = CreateProxyEventHandler(eventInfo, eventPublisher, eventHandler);

            eventInfo.GetAddMethod().Invoke(eventPublisher, new[] { proxyEventHandler });
        }

        #endregion
    }
}